﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;

namespace CardPlay.Model.ActionModel
{
    public abstract class ActionHistory : IActionHistory
    {
        private interface IUnitOfWork
        {
            void Log(IAction action);
            void Commit();
        }

        private sealed class UnitOfWork : IUnitOfWork
        {
            private sealed class TransactionScope : ITransactionScope
            {
                private IUnitOfWork _unitOfWork;

                public TransactionScope(IUnitOfWork unitOfWork)
                {
                    _unitOfWork = unitOfWork;
                }

                public void Execute(IAction action)
                {
                    _unitOfWork.Log(action);
                }

                public void Dispose()
                {
                    if (_unitOfWork == null) return;

                    _unitOfWork.Commit();
                    _unitOfWork = null;
                }
            }

            private readonly List<IAction> _script = new List<IAction>();
            private readonly ActionHistory _executionContext;

            public UnitOfWork(ActionHistory executionContext)
            {
                _executionContext = executionContext;
            }

            public object PriorState { get; set; }

            public IEnumerable<IAction> Actions { get { return _script; } }

            public ITransactionScope CreateTransactionScope()
            {
                var result = new TransactionScope(this);

                ++OutstandingCommits;
                return result;
            }

            void IUnitOfWork.Log(IAction action)
            {
                Guard.Against<ArgumentNullException>(action == null, "action");
                Guard.Against<InvalidOperationException>(OutstandingCommits == 0, "action");

                // ReSharper disable PossibleNullReferenceException
                _script.Add(action);
                action.Execute();
                // ReSharper restore PossibleNullReferenceException
            }

            void IUnitOfWork.Commit()
            {
                Guard.Against<InvalidOperationException>(OutstandingCommits == 0, "Unbalanced call to Commit");

                if (OutstandingCommits == 1) 
                    _executionContext.Commit(this);

                --OutstandingCommits;
            }

            private int OutstandingCommits { get; set; }
        };

        private readonly List<IAction> _actionSequence = new List<IAction>();
        private int _totalInUndoStack;
        private UnitOfWork _activeUnitOfWork;

        public event EventHandler TransactionEnding;

        public bool InTransaction
        {
            get { return _activeUnitOfWork != null; }
        }

        public int TotalChanges
        {
            get; private set;
        }

        public ITransactionScope TransactionScope
        {
            get
            {
                if (_activeUnitOfWork == null) _activeUnitOfWork = new UnitOfWork(this) { PriorState = CreateSnapshot() };
                return _activeUnitOfWork.CreateTransactionScope();
            }
        }

        public void Clear()
        {
            _actionSequence.Clear();
            TotalInUndoStack = 0;
        }

        #region IChangeHistory Members

        public bool CanUndo
        {
            get { return IsChangePermitted && !InTransaction && TotalInUndoStack > 0; }
        }

        public bool CanRedo
        {
            get { return IsChangePermitted && !InTransaction && TotalInUndoStack < _actionSequence.Count; }
        }

        public void Undo()
        {
            Invoke(() => _actionSequence[--TotalInUndoStack].Reverse());
        }

        public void Redo()
        {
            Invoke(() => _actionSequence[TotalInUndoStack++].Execute());
        }

        #endregion

        protected abstract bool IsChangePermitted { get; }

        protected abstract object CreateSnapshot();

        protected abstract void OnStateChanged(object priorState);

        private int TotalInUndoStack
        {
            get { return _totalInUndoStack; }
            set
            {
                Guard.Against<ArgumentOutOfRangeException>(value < 0, "value cannot be negative");

                _totalInUndoStack = value;
                ++TotalChanges;
            }
        }

        private void Invoke(Action update)
        {
            var priorState = CreateSnapshot();
            update();
            OnStateChanged(priorState);
        }

        private void Commit(UnitOfWork work)
        {
            Contract.Requires<InvalidOperationException>(_activeUnitOfWork == work, "work");

            var callback = TransactionEnding;
            if (callback != null) callback(this, EventArgs.Empty);

            _activeUnitOfWork = null;

            if (!work.Actions.Any()) return;

            _actionSequence.RemoveRange(TotalInUndoStack, Math.Max(0, _actionSequence.Count - TotalInUndoStack));
            _actionSequence.Add(work.Actions.Count() == 1 ? work.Actions.First() : new ComplexAction(work.Actions));
            
            ++TotalInUndoStack;

            OnStateChanged(work.PriorState);
        }
    }
}
